package sk.sodik.atomFilter.feeds;

import java.io.InputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Logger;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.ScriptableObject;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import sk.sodik.atomFilter.LogUtil;
import sk.sodik.atomFilter.util.XMLDocumentWriter;

public class Feed {
	private static final Logger log = Logger.getLogger(Feed.class.getName());
	private String url;
	private Document doc = null;

	public Feed(String url) {
		super();
		this.url = url;
	}

	public boolean download() {
		try {
			URL netUrl = new URL(url);

			FeedCache cache = FeedCache.instance;
			doc = cache.getFeedForUrl(netUrl);

			if (doc == null) {
				InputStream stream = netUrl.openStream();
				log.fine("Document downloading from net");
				parseXML(stream);
				stream.close();
				
				cache.setFeedForUrl(netUrl, doc);
			} else {
				log.fine("Document retrieved from cache");
			}

		} catch (Exception e) {
			LogUtil.exception(log, e);
			return false;
		}
		return true;
	}

	private void parseXML(InputStream is) throws Exception {
		DocumentBuilder builder = DocumentBuilderFactory.newInstance()
				.newDocumentBuilder();
		doc = builder.parse(is);
		translateRelativePaths();
	}

	/**
	 * Translates relative paths in HREF attributes to the absolute depending on
	 * the download url.
	 * 
	 * Remark: It seems, that sometimes this does not helps since some resources
	 * can't be get from another domain.
	 */
	private void translateRelativePaths() {
		try {
			URL baseUrl = new URL(url);

			translateRelativePaths(baseUrl, doc);
		} catch (Exception e) {
			LogUtil.exception(log, e);
		}
	}

	private void translateRelativePaths(URL baseUrl, Node node)
			throws MalformedURLException {
		if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
			if ("href".equalsIgnoreCase(node.getNodeName())) {
				String href = node.getNodeValue();
				URL absoluteUrl = new URL(baseUrl, href);
				// System.out.println(href + " translated to " + absoluteUrl);
				node.setNodeValue(absoluteUrl.toExternalForm());
			}
		}
		if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
			// search for href in ProcessingInstruction.data
			String data = node.getNodeValue();
			int i = data.indexOf("href=\"");
			if (i != -1) {
				// find end
				int j = data.indexOf('"', i + 6);
				if (j != -1) {
					String href = data.substring(i + 6, j);
					URL absoluteUrl = new URL(baseUrl, href);
					String newData = data.substring(0, i + 6)
							+ absoluteUrl.toExternalForm() + data.substring(j);
					// System.out.println(data + " changed to " + newData);
					node.setNodeValue(newData);
				}
			}
		}

		NamedNodeMap attributes = node.getAttributes();
		if (attributes != null) {
			for (int i = 0, cnt = attributes.getLength(); i < cnt; i++) {
				Node child = attributes.item(i);
				translateRelativePaths(baseUrl, child);
			}
		}

		NodeList nodeList = node.getChildNodes();

		for (int i = 0, cnt = nodeList.getLength(); i < cnt; i++) {
			Node child = nodeList.item(i);
			translateRelativePaths(baseUrl, child);
		}

	}

	public boolean filter(String jsExpression) {
		try {
			NodeList rootElements = doc.getChildNodes();
			Node feedElement = null;
			for (int i = 0, cnt = rootElements.getLength(); i < cnt; i++) {
				Node node = rootElements.item(i);
				if (node.getNodeType() == Node.ELEMENT_NODE
						&& "feed".equals(node.getNodeName())) {
					feedElement = node;
					break;
				}
			}
			if (feedElement == null) {
				log.warning("Cannot find FEED element");
				return false;
			}

			// search for entry elements in the reverse order
			NodeList nodeList = feedElement.getChildNodes();
			for (int i = nodeList.getLength() - 1; i >= 0; i--) {
				Node node = nodeList.item(i);
				if (node.getNodeType() == Node.ELEMENT_NODE
						&& "entry".equals(node.getNodeName())) {
					boolean delete = !eval(node, jsExpression);
					if (delete) {
						feedElement.removeChild(node);
					}
				}
			}
		} catch (Exception e) {
			LogUtil.exception(log, e);
			return false;
		}
		return true;
	}

	private boolean eval(Node node, String jsExpression) {
		Context cx = Context.enter();

		try {
			ScriptableObject scope = cx.initStandardObjects();

			// fill in all children as string
			NodeList nodeList = node.getChildNodes();

			for (int i = 0, cnt = nodeList.getLength(); i < cnt; i++) {
				Node child = nodeList.item(i);
				if (child.getNodeType() == Node.ELEMENT_NODE) {
					String name = child.getNodeName();
					String text = child.getTextContent();
					ScriptableObject.putProperty(scope, name, text);
				}
			}

			Object obj = cx.evaluateString(scope, jsExpression, "<cmd>", 1,
					null);
			// System.out.println(obj);
			// System.out.println(obj.getClass().getName());
			if (obj == null) {
				return false;
			}
			if (obj instanceof Boolean) {
				return ((Boolean) obj).booleanValue();
			}
			if (obj instanceof Number) {
				return ((Number) obj).intValue() >= 0;
			}
			return obj.toString().length() > 0;
		} catch (Exception e) {
			LogUtil.exception(log, e);
			return true;
		} finally {
			Context.exit();
		}
	}

	/* it is working locally, but on GAE engine is null! */
	private boolean eval16(Node node, String jsExpression) {
		ScriptEngineManager manager = new ScriptEngineManager();
		ScriptEngine engine = manager.getEngineByName("JavaScript");

		NodeList nodeList = node.getChildNodes();

		for (int i = 0, cnt = nodeList.getLength(); i < cnt; i++) {
			Node child = nodeList.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				String name = child.getNodeName();
				String text = child.getTextContent();
				engine.put(name, text);
			}
		}

		try {
			Object obj = engine.eval(jsExpression);
			// System.out.println(obj);
			// System.out.println(obj.getClass().getName());
			if (obj == null) {
				return false;
			}
			if (obj instanceof Boolean) {
				return ((Boolean) obj).booleanValue();
			}
			if (obj instanceof Number) {
				return ((Number) obj).intValue() >= 0;
			}
			return obj.toString().length() > 0;
		} catch (ScriptException e) {
			LogUtil.exception(log, e);
			return true;
		}
	}

	public void print(PrintWriter writer) {
		// printAsText(writer, doc, 0);
		// output XML!
		printViaXLS(writer);

		// printXmlNice(writer);
	}

	private void printXmlNice(PrintWriter writer) {
		XMLDocumentWriter wr = new XMLDocumentWriter(writer);
		wr.write(doc);
	}

	/* tak funguje to... ;-) */
	private void printViaXLS(PrintWriter writer)
			throws TransformerFactoryConfigurationError {
		DOMSource source = new DOMSource(doc);
		StreamResult result = new StreamResult(writer);

		try {
			Transformer xformer = TransformerFactory.newInstance()
					.newTransformer();
			xformer.transform(source, result);
		} catch (Exception e) {
			LogUtil.exception(log, e);
		}
	}

	private void printAsText(PrintWriter writer, Node node, int level) {
		NodeList nodeList = node.getChildNodes();

		for (int i = 0, cnt = nodeList.getLength(); i < cnt; i++) {
			for (int j = 0; j < level; j++) {
				writer.print(' ');
			}
			writer.print("[" + nodeList.item(i) + "]\n");
			printAsText(writer, nodeList.item(i), level + 1);
		}
	}

}
